package k8s

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"strconv"

	"gitee.com/rdor/pod-filebrowser/pkg/fileinfo"
	"gitee.com/rdor/pod-filebrowser/pkg/types"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/remotecommand"
)

type Client struct {
	clientset *kubernetes.Clientset
	config    *rest.Config
}

func NewClient() (*Client, error) {
	config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
	if err != nil {
		return nil, err
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	return &Client{
		clientset: clientset,
		config:    config,
	}, nil
}

func (c *Client) ListNamespaces() ([]types.Namespace, error) {
	namespaces, err := c.clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	result := make([]types.Namespace, len(namespaces.Items))
	for i, ns := range namespaces.Items {
		result[i] = types.Namespace{Name: ns.Name}
	}
	return result, nil
}

func (c *Client) ListPods(namespace string) ([]types.Pod, error) {
	pods, err := c.clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	result := make([]types.Pod, len(pods.Items))
	for i, pod := range pods.Items {
		result[i] = types.Pod{
			Name:      pod.Name,
			Namespace: pod.Namespace,
			Status:    string(pod.Status.Phase),
		}
	}
	return result, nil
}

func (c *Client) ListContainers(namespace, podName string) ([]types.Container, error) {
	pod, err := c.clientset.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	result := make([]types.Container, len(pod.Spec.Containers))
	for i, container := range pod.Spec.Containers {
		result[i] = types.Container{
			Name:    container.Name,
			PodName: podName,
			Image:   container.Image,
		}
	}
	return result, nil
}

func (c *Client) GetPod(namespace, podName string) (*corev1.Pod, error) {
	ctx := context.Background()
	return c.clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
}

// Stat returns file info for the given path in container
func (c *Client) Stat(pod *corev1.Pod, containerID, path string) (os.FileInfo, error) {
	command := []string{"stat", "-c", "%A %h %U %G %s %Y %n", path}
	output, err := c.execCommand(pod, containerID, command)
	if err != nil {
		return nil, err
	}

	// TODO
	fields := bytes.Fields(output)
	if len(fields) < 7 {
		return nil, fmt.Errorf("invalid stat output: %s", string(output))
	}
	info := fileinfo.New(string(fields[6]), parseSize(fields[4]))
	return info, nil
}

// ReadDir returns directory entries
func (c *Client) ReadDir(pod *corev1.Pod, containerID, path string) ([]os.FileInfo, error) {
	command := []string{"ls", "-l", path}
	output, err := c.execCommand(pod, containerID, command)
	if err != nil {
		return nil, err
	}

	lines := bytes.Split(output, []byte("\n"))
	var infos []os.FileInfo
	for _, line := range lines {
		if len(line) == 0 {
			continue
		}
		fields := bytes.Fields(line)
		if len(fields) < 9 {
			continue
		}

		info := fileinfo.New(string(fields[8]), parseSize(fields[4]))
		infos = append(infos, info)
	}
	return infos, nil
}

// ReadFile reads entire file content
func (c *Client) ReadFile(pod *corev1.Pod, containerID, path string) ([]byte, error) {
	command := []string{"cat", path}
	return c.execCommand(pod, containerID, command)
}

func (c *Client) execCommand(pod *corev1.Pod, containerID string, command []string) ([]byte, error) {
	req := c.clientset.CoreV1().RESTClient().Post().
		Resource("pods").
		Name(pod.Name).
		Namespace(pod.Namespace).
		SubResource("exec").
		VersionedParams(&corev1.PodExecOptions{
			Container: containerID,
			Command:   command,
			Stdin:     false,
			Stdout:    true,
			Stderr:    true,
			TTY:       false,
		}, metav1.ParameterCodec)

	exec, err := remotecommand.NewSPDYExecutor(c.config, "POST", req.URL())
	if err != nil {
		return nil, err
	}

	var stdout, stderr bytes.Buffer
	err = exec.Stream(remotecommand.StreamOptions{
		Stdin:  nil,
		Stdout: &stdout,
		Stderr: &stderr,
		Tty:    false,
	})
	if err != nil {
		return nil, fmt.Errorf("exec command failed: %v, stderr: %s", err, stderr.String())
	}

	return stdout.Bytes(), nil
}

func parseSize(size []byte) int64 {
	s, err := strconv.ParseInt(string(size), 10, 64)
	if err != nil {
		return 0
	}
	return s
}
